Object-Relational Metadata Mapping Patterns
I18n And L10n
Text Information in the Database
Unit And Functional Testing
Chapter 13 of PEAA
Holds details of object-relational mapping in metadata.
Query Object
An object that represents a query
Repository
between the domain and data mapping layers
You build the query and give it to repository to return or objects that
satisfy that query
public class Person
{
public List dependents
() {
Repository repository
= Registry
.personRepository
();
Criteria criteria
= new Criteria
();
criteria
.equal
(Person
.BENEFACTOR
, this
);
return repository
.matching
(criteria
);
}
}
Client never thinks of queries even in OO sense.
Separations: Model/View (
very important) - View/Controller
Page Controller
An object that handles a request for a specific page or action on a Web site
front Controller
A controller that handles all requests for a Web site
Template View
Renders information into HTML by embedding markers in an HTML page.
Transform View
A view that processes domain data element by element and transforms it into HTML.
Two Step View
Turns domain data into HTML in two steps: first by forming some kind of logical page, then rendering the logical page into HTML.
test
i18n: several versions of the same content in various languages
l10n: distinct information according to the country from which it is browsed
Services
- Text Translation
- Standards and formats
- Localized Content
Culture
all:
.settings:
default_culture: fr_FR
combination of country/language to specify different people
setting/getting:
// Culture setter
$this->getUser()->setCulture('en_US');
// Culture getter
$culture = $this->getUser()->getCulture();
=> en_US
Standards and Formats
Numbers:
<?php use_helper
('Number') ?>
<?php $sf_user->setCulture('en_US') ?>
<?php echo format_number
(12000.10) ?>
=> '12,000.10'
<?php $sf_user->setCulture('fr_FR') ?>
<?php echo format_number
(12000.10) ?>
=> '12 000,10'
Dates:
<?php use_helper
('Date') ?>
<?php echo format_date
(time()) ?>
=> '9/14/06'
<?php echo format_datetime
(time()) ?>
=> 'September 14, 2006 6:11:07 PM CEST'
<?php use_helper
('Number') ?>
<?php echo format_number
(12000.10) ?>
=> '12,000.10'
<?php echo format_currency
(1350, 'USD') ?>
=> '$1,350.00'
<?php use_helper
('I18N') ?>
<?php echo format_country
('US') ?>
=> 'United States'
<?php format_language
('en') ?>
=> 'English'
<?php use_helper
('Form') ?>
<?php echo input_date_tag
('birth_date', mktime(0, 0, 0, 9, 14, 2006)) ?>
=> input type="text" name="birth_date" id="birth_date" value="9/14/06" size="11" />
<?php echo select_country_tag
('country', 'US') ?>
=> <select name="country" id="country"><option value="AF">Afghanistan</option>
...
<option value="GB">United Kingdom</option>
<option value="US" selected="selected">United States</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
...
</select>
Getting info from a localized data
$date= $request->getParameter('birth_date');
$user_culture = $this->getUser()->getCulture();
// Getting a timestamp
$timestamp = $this->getContext()->getI18N()->getTimestampForCulture($date, $user_culture);
// Getting a structured date
list($d, $m, $y) = $this->getContext()->getI18N()->getDateForCulture($date, $user_culture); Assume one field of a table is culture dependent.
Localized Scheme
Two tables: one for localized data and one for others.
my_connection:
my_product:
_attributes: { phpName: Product, isI18N: true, i18nTable: my_product_i18n }
id: { type: integer, required: true, primaryKey: true, autoincrement: true }
price: { type: float }
my_product_i18n:
_attributes: { phpName: ProductI18n }
id: { type: integer, required: true, primaryKey: true, foreignTable: my_product, foreignReference: id }
culture: { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
name: { type: varchar, size: 50 }
or, much simpler
my_connection:
my_product:
_attributes: { phpName: Product }
id:
price: float
my_product_i18n:
_attributes: { phpName: ProductI18n }
name: varchar(50)
Working with generated objects
$product = ProductPeer
::retrieveByPk(1);
$product->setName('Nom du produit'); // By default, the culture is the current user culture
$product->save();
echo $product->getName();
=> 'Nom du produit'
$product->setName('Product name', 'en'); // change the value for the 'en' culture
$product->save();
echo $product->getName('en');
=> 'Product name' Making a Query
$c = new Criteria();
$c->add(ProductPeer::PRICE, 100, Criteria::LESS_THAN);
$products = ProductPeer::doSelectWithI18n($c, $culture);
// The $culture argument is optional
// The current user culture is used if no culture is given
Step 1: Activation
frontend/config/settings.yml
all:
.settings:
i18n: on
Step 2: get ready
Instead of
Welcome to our website. Today's date is
<?php echo format_date
(date()) ?>
Write
<?php use_helper
('I18N') ?>
<?php echo __
('Welcome to our website.') ?>
<?php echo __
("Today's date is ") ?>
<?php echo format_date
(date()) ?> Step 3: Dictionary Files
XML Localization Interchange File Format (XLIFF) files messages.[language code].xml
frontend/i18n/messages.fr.xml
<?xml version="1.0" ?>
<xliff version="1.0">
<file original="global" source-language="en_US" datatype="plaintext">
<body>
<trans-unit id="1">
<source>Welcome to our website.</source>
<target>Bienvenue sur notre site web.</target>
</trans-unit>
<trans-unit id="2">
<source>Today's date is </source>
<target>La date d'aujourd'hui est </target>
</trans-unit>
</body>
</file>
</xliff>
Use several files
e.g., navigation.ir.xml
<?php echo __
('Welcome to our website', null, 'navigation') ?> symfony 1.2 specific utilities
message file auto generation
php symfony i18n
:extract frontend en
Culture dependent image
<?php echo image_tag
($sf_user->getCulture().'/myText.gif') ?> Handling Large Sentences
// Base example
Welcome to all the <b>new</b> users.<br />
There are
<?php echo count_logged
() ?> persons logged.
// Bad way to enable text translation
<?php echo __
('Welcome to all the') ?>
<b>
<?php echo __
('new') ?></b>
<?php echo __
('users') ?>.<br />
<?php echo __
('There are') ?>
<?php echo count_logged
() ?>
<?php echo __
('persons logged') ?>
// Good way to enable text translation
<?php echo __
('Welcome to all the <b>new</b> users') ?> <br />
<?php echo __
('There are %1% persons logged', array('%1%' => count_logged
())) ?>files end in Test.php located in /test/unit directory
test/unit/strtolowerTest.php
<?php
include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once(dirname(__FILE__).'/../../lib/strtolower.php');
$t = new lime_test
(7, new lime_output_color
());
// strtolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string',
'strtolower() returns a string');
$t->is(strtolower('FOO'), 'foo',
'strtolower() transforms the input to lowercase');
$t->is(strtolower('foo'), 'foo',
'strtolower() leaves lowercase characters unchanged');
$t->is(strtolower('12#?@~'), '12#?@~',
'strtolower() leaves non alphabetical characters unchanged');
$t->is(strtolower('FOO BAR'), 'foo bar',
'strtolower() leaves blanks alone');
$t->is(strtolower('FoO bAr'), 'foo bar',
'strtolower() deals with mixed case input');
$t->is(strtolower(''), 'foo',
'strtolower() transforms empty strings into foo');
Testing
> php symfony test
:unit
strtolower
1
..7
# strtolower()
ok
1 - strtolower() returns a string
ok
2 - strtolower() transforms the input to lowercase
ok
3 - strtolower() leaves lowercase characters unchanged
ok
4 - strtolower() leaves non alphabetical characters unchanged
ok
5 - strtolower() leaves blanks alone
ok
6 - strtolower() deals with mixed
case input
not ok
7 - strtolower() transforms
empty strings into foo
# Failed test (.\batch\test.php at line 21)
# got: ''
# expected: 'foo'
# Looks like you failed 1 tests of 7.
unit test functions
Method |
Description |
diag($msg) |
Outputs a diag message but runs no test |
ok($test[, $msg]) |
Tests a condition and passes if it is true |
is($value1, $value2[, $msg]) |
Compares two values and passes if they are equal (== ) |
isnt($value1, $value2[, $msg]) |
Compares two values and passes if they are not equal |
like($string, $regexp[, $msg]) |
Tests a string against a regular expression |
unlike($string, $regexp[, $msg]) |
Checks that a string doesn't match a regular expression |
cmp_ok($value1, $operator, $value2[, $msg]) |
Compares two arguments with an operator |
isa_ok($variable, $type[, $msg]) |
Checks the type of an argument |
isa_ok($object, $class[, $msg]) |
Checks the class of an object |
can_ok($object, $method[, $msg]) |
Checks the availability of a method for an object or a class |
is_deeply($array1, $array2[, $msg]) |
Checks that two arrays have the same values |
include_ok($file[, $msg]) |
Validates that a file exists and that it is properly included |
fail([$msg]) |
Always fails--useful for testing exceptions |
pass([$msg]) |
Always passes--useful for testing exceptions |
skip([$msg, $nb_tests]) |
Counts as $nb_tests tests--useful for conditional tests |
todo([$msg]) |
Counts as a test--useful for tests yet to be written |
comment($msg) |
Outputs a comment message but runs no test |
error($msg) |
Outputs a error message but runs no test |
info($msg) |
Outputs a info message but runs no test |
test:unit task
// Test directory structure
test/
unit/
myFunctionTest.php
mySecondFunctionTest.php
foo/
barTest.php
> php symfony test:unit myFunction ## Run myFunctionTest.php
> php symfony test:unit myFunction mySecondFunction ## Run both tests
> php symfony test:unit 'foo/*' ## Run barTest.php
> php symfony test:unit '*' ## Run all tests (recursive)
Testing Propel
<?php
include(dirname(__FILE__).'/../bootstrap/unit.php');
new sfDatabaseManager
(ProjectConfiguration
::getApplicationConfiguration('frontend', 'test', true));
$loader = new sfPropelData
();
$loader->loadData(sfConfig
::get('sf_data_dir').'/fixtures');
$t = new lime_test
(1, new lime_output_color
());
// begin testing your model class
$t->diag('->retrieveByUsername()');
$user = UserPeer
::retrieveByUsername('fabien');
$t->is($user->getLastName(), 'Potencier', '->retrieveByUsername() returns the User for the given username'); simulate a browsing session, make requests, and check elements in the response
symfony provides you with sfTestBrowser class.
tests/functional/frontend/foobarActionsTest.php
<?php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
// Create a new test browser
$browser = new sfTestBrowser
();
$browser->
get('/foobar/index')->
isStatusCode(200)->
isRequestParameter('module', 'foobar')->
isRequestParameter('action', 'index')->
checkResponseElement('body', '!/This is a temporary page/')
; calling test
> php symfony test:functional frontend foobarActions
# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
not ok 4 - response selector body does not match regex /This is a temporary page/
# Looks like you failed 1 tests of 4.
1..4
using TestBrowser
include(dirname(__FILE__).'/../../bootstrap/functional.php');
// Create a new test browser
$b = new sfTestBrowser
();
$b->get('/foobar/show/id/1'); // GET request
$b->post('/foobar/show', array('id' => 1)); // POST request
// The get() and post() methods are shortcuts to the call() method
$b->call('/foobar/show/id/1', 'get');
$b->call('/foobar/show', 'post', array('id' => 1));
// The call() method can simulate requests with any method
$b->call('/foobar/show/id/1', 'head');
$b->call('/foobar/add/id/1', 'put');
$b->call('/foobar/delete/id/1', 'delete'); Do Normal Browsing
$b->get('/'); // Request to the home page
$b->get('/foobar/show/id/1');
$b->back(); // Back to one page in history
$b->forward(); // Forward one page in history
$b->reload(); // Reload current page
$b->click('go'); // Look for a 'go' link or button and click it
Forms
// Example template in modules/foobar/templates/editSuccess.php
<?php echo form_tag
('foobar/update') ?>
<?php echo input_hidden_tag
('id', $sf_params->get('id')) ?>
<?php echo input_tag
('name', 'foo') ?>
<?php echo submit_tag
('go') ?>
<?php echo textarea
('text1', 'foo') ?>
<?php echo textarea
('text2', 'bar') ?>
</form>
// Example functional test for this form
$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1');
// Option 1: POST request
$b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));
// Option 2: Click the submit button with parameters
$b->click('go', array('name' => 'dummy'));
// Option 3: Enter the form values field by field name then click the submit button
$b->setField('name', 'dummy')->
click('go');
assertions
$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1');
$request = $b->getRequest();
$context = $b->getContext();
$response = $b->getResponse();
// Get access to the lime_test methods via the test() method
$b->test()->is($request->getParameter('id'), 1);
$b->test()->is($response->getStatuscode(), 200);
$b->test()->is($response->getHttpHeader('content-type'), 'text/html;charset=utf-8');
$b->test()->like($response->getContent(), '/edit/');